use std::borrow::Cow;

use crate::{generation::SymbolMapGeneration, mapped_path::UnparsedMappedPath};

#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct SourceFilePathIndex(pub u32);

/// A handle for a [`SourceFilePath`]. Can be resolved with the symbol map.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct SourceFilePathHandle {
    pub(crate) generation: SymbolMapGeneration,
    pub(crate) index: SourceFilePathIndex,
}

/// The path of a source file, as found in the debug info.
///
/// This contains both the raw path and an optional "mapped path". The raw path can
/// refer to a file on this machine or on a different machine (i.e. the original
/// build machine). The mapped path is something like a permalink which potentially
/// allows obtaining the source file from a source server or a public hosted repository.
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
pub enum SourceFilePath<'a> {
    RawPath(Cow<'a, str>),
    BreakpadSpecialPathStr(Cow<'a, str>),
    RawPathAndUrl(Cow<'a, str>, Cow<'a, str>),
}

impl<'a> SourceFilePath<'a> {
    /// A short, display-friendly version of this path.
    pub fn display_path(&self) -> Cow<'_, str> {
        if let Some(display_path) = self.mapped_path().display_path() {
            let display_path = display_path.to_string();
            return display_path.into();
        }
        Cow::Borrowed(self.raw_path())
    }

    pub fn to_owned(&self) -> SourceFilePath<'static> {
        match self {
            Self::RawPath(r) => SourceFilePath::RawPath(r.as_ref().to_owned().into()),
            Self::BreakpadSpecialPathStr(bsp) => {
                SourceFilePath::BreakpadSpecialPathStr(bsp.as_ref().to_owned().into())
            }
            Self::RawPathAndUrl(r, u) => SourceFilePath::RawPathAndUrl(
                r.as_ref().to_owned().into(),
                u.as_ref().to_owned().into(),
            ),
        }
    }

    pub fn special_path_str(&self) -> Option<Cow<'_, str>> {
        if let Self::BreakpadSpecialPathStr(bsp) = self {
            return Some(Cow::Borrowed(bsp));
        }
        if let Some(bsp) = self.mapped_path().special_path_str() {
            let bsp = bsp.to_string();
            return Some(bsp.into());
        }
        None
    }

    /// The raw path to the source file, as written down in the debug file. This is
    /// usually an absolute path.
    ///
    /// Examples:
    ///
    ///  - `"/Users/mstange/code/samply/samply-symbols/src/shared.rs"`
    ///  - `"/Users/mstange/code/mozilla/widget/cocoa/nsNativeThemeCocoa.mm"`
    ///  - `"./csu/../csu/libc-start.c"`
    ///  - `"/rustc/69f9c33d71c871fc16ac445211281c6e7a340943/library/core/src/ptr/const_ptr.rs"`
    ///  - `r#"D:\agent\_work\2\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl"#`
    ///
    /// If the debug file was produced by compiling code on this machine, then the path
    /// usually refers to a file on this machine. (An exception to this is debug info
    /// from the Rust stdlib, which has fake `/rustc/<rev>/...` paths even if the when
    /// compiling Rust code locally.)
    ///
    /// If the code was compiled on a different machine, then the raw path does not refer
    /// to a file on this machine.
    ///
    /// Sometimes this path is a relative path. One such case was observed when the
    /// "debug file" was a synthetic .so file which was generated by `perf inject --jit`
    /// based on a JITDUMP file which included relative paths. You could argue
    /// that the application which emitted relative paths into the JITDUMP file was
    /// creating bad data and should have written out absolute paths. However, the `perf`
    /// infrastructure worked fine on this file, because the relative paths happened to
    /// be relative to the working directory, and because perf / objdump were resolving
    /// those relative paths relative to the current working directory.
    pub fn raw_path(&self) -> &str {
        match self {
            SourceFilePath::RawPath(raw) => raw,
            SourceFilePath::BreakpadSpecialPathStr(bsp) => bsp, // is this ok?
            SourceFilePath::RawPathAndUrl(raw, _) => raw,
        }
    }

    /// Returns the raw path while consuming this `SourceFilePath`.
    pub fn into_raw_path(self) -> Cow<'a, str> {
        match self {
            SourceFilePath::RawPath(raw) => raw,
            SourceFilePath::BreakpadSpecialPathStr(bsp) => bsp, // is this ok?
            SourceFilePath::RawPathAndUrl(raw, _) => raw,
        }
    }

    /// A variant of the path which may allow obtaining the source code for this file
    /// from the web. The return value
    ///
    /// Examples:
    ///
    ///   - If the source file is from a Rust dependency from crates.io, we detect the
    ///     cargo cache directory in the raw path and create a mapped path of the form [`MappedPath::Cargo`](super::mapped_path::MappedPath::Cargo).
    ///   - If the source file can be obtained from a github URL, and we know this either
    ///     from the `srcsrv` stream of a PDB file or because we recognize a path of the
    ///     form `/rustc/<rust-revision>/`, then we create a mapped path of the form [`MappedPath::Git`](super::mapped_path::MappedPath::Git).
    pub fn mapped_path(&self) -> UnparsedMappedPath<'_> {
        match self {
            SourceFilePath::RawPath(raw) => UnparsedMappedPath::RawPath(raw.clone()),
            SourceFilePath::BreakpadSpecialPathStr(bsp) => {
                UnparsedMappedPath::BreakpadSpecialPath(bsp.clone())
            }
            SourceFilePath::RawPathAndUrl(_, url) => UnparsedMappedPath::Url(url.clone()),
        }
    }
}

impl SymbolMapGeneration {
    pub fn source_file_handle(&self, index: SourceFilePathIndex) -> SourceFilePathHandle {
        SourceFilePathHandle {
            generation: *self,
            index,
        }
    }

    pub fn unwrap_source_file_index(&self, handle: SourceFilePathHandle) -> SourceFilePathIndex {
        assert_eq!(
            handle.generation, *self,
            "SourceFilePathHandle from wrong symbol map used"
        );
        handle.index
    }
}
